import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:async/async.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../callback/impl.dart';
import '../constants/constants.dart';
import '../constants/task_status.dart';
import '../db/task_entity.dart';

/// Uploads large files by chunking them into smaller parts
class UploaderCore {
  final Dio _dio;

  const UploaderCore(this._dio);

  /// Uploads the file using it's data stream
  /// Suitable for Web platform since the file path isn't available
  Future<Response?> fromFileBytes({
    required Stream<List<int>> fileDataStream,
    required TaskEntity task,
    Map<String, dynamic>? data,
    CancelToken? cancelToken,
    int? maxChunkSize,
    Function(TaskEntity, double)? onUploadProgress,
    ChunkHeadersCallback? headersCallback,
    String method = 'POST',
    String fileKey = 'file',
  }) =>
      _Uploader.fromFileBytes(
        _dio,
        fileDataStream: fileDataStream,
        fileKey: fileKey,
        method: method,
        data: data,
        cancelToken: cancelToken,
        maxChunkSize: maxChunkSize,
        onUploadProgress: onUploadProgress,
        headersCallback: headersCallback,
        task: task,
      ).upload();

  /// Uploads the file using it's path
  Future<Response?> fromFilePath({
    required TaskEntity task,
    Map<String, dynamic>? data,
    CancelToken? cancelToken,
    int? maxChunkSize,
    Function(TaskEntity, double)? onUploadProgress,
    ChunkHeadersCallback? headersCallback,
    String method = 'POST',
    String fileKey = 'file',
  }) =>
      _Uploader.fromFilePath(
        _dio,
        task: task,
        fileKey: fileKey,
        method: method,
        data: data,
        cancelToken: cancelToken,
        maxChunkSize: maxChunkSize,
        onUploadProgress: onUploadProgress,
        headersCallback: headersCallback,
      ).upload();
}

class _Uploader {
  final Dio dio;
  late final ChunkedStreamReader<int> streamReader;
  final String fileKey;
  final TaskEntity task;
  final String? method;
  final Map<String, dynamic>? data;
  final CancelToken? cancelToken;
  final Function(TaskEntity, double)? onUploadProgress;
  late int _maxChunkSize;
  final ChunkHeadersCallback _headersCallback;

  _Uploader(
    this.dio,
    this.streamReader,
    this._headersCallback, {
    required this.task,
    required Stream<List<int>> fileDataStream,
    required this.fileKey,
    this.method,
    this.data,
    this.cancelToken,
    this.onUploadProgress,
    ChunkHeadersCallback? headersCallback,
    int? maxChunkSize,
  });

  _Uploader.fromFileBytes(
    this.dio, {
    required Stream<List<int>> fileDataStream,
    required this.task,
    required this.fileKey,
    this.method,
    this.data,
    this.cancelToken,
    this.onUploadProgress,
    ChunkHeadersCallback? headersCallback,
    int? maxChunkSize,
  })  : streamReader = ChunkedStreamReader(fileDataStream),
        _maxChunkSize = min(task.fileSize, maxChunkSize ?? task.fileSize),
        _headersCallback = headersCallback ?? _defaultHeadersCallback;

  _Uploader.fromFilePath(
    this.dio, {
    required this.task,
    required this.fileKey,
    this.method,
    this.data,
    this.cancelToken,
    this.onUploadProgress,
    ChunkHeadersCallback? headersCallback,
    int? maxChunkSize,
  }) : _headersCallback = headersCallback ?? _defaultHeadersCallback {
    final file = File(task.filePath ?? '');
    debugPrint('fileSize: ${file.lengthSync()}');
    streamReader = ChunkedStreamReader(file.openRead());
    _maxChunkSize = min(task.fileSize, maxChunkSize ?? task.fileSize);
  }

  Future<Response?> upload() async {
    try {
      Response? finalResponse;
      final formData = FormData.fromMap({});
      final MultipartFile file = await MultipartFile.fromFile(task.filePath);
      formData.files.add(MapEntry(Constants.file, file));
      finalResponse = await dio.post(
        task.url,
        data: formData,
        cancelToken: cancelToken,
        onSendProgress: (current, total) => _progress(current, total),
      );
      return finalResponse;
    } catch (e) {
      task.errorMessage = e.toString();
      task.status = TaskStatus.failed.name;
      onUploadProgress?.call(task, task.progress ?? 0.0);
      debugPrint(e.toString());
      rethrow;
    } finally {
      streamReader.cancel();
    }
  }

  Future<Response?> uploadChunk() async {
    try {
      Response? finalResponse;
      for (int i = 0; i < _chunksCount; i++) {
        final start = _getChunkStart(i);
        final end = _getChunkEnd(i);
        final chunkStream = _getChunkStream();
        final formData = FormData.fromMap({
          task.fileName:
              MultipartFile(chunkStream, end - start, filename: task.fileName),
          if (data != null) ...data!
        });
        final headers = dio.options.headers
          ..addAll(_headersCallback(start, end, task.fileSize));
        // final MultipartFile file = await MultipartFile.fromFile(task.filePath);
        // formData.files.add(MapEntry(Constants.file, file));
        finalResponse = await dio.post(
          task.url,
          data: formData,
          cancelToken: cancelToken,
          options: Options(
            method: method,
            headers: headers,
          ),
          onSendProgress: (current, total) =>
              _updateProgress(i, current, total),
        );
      }
      return finalResponse;
    } catch (e) {
      onUploadProgress?.call(task, task.progress ?? 0.0);
      debugPrint(e.toString());
      rethrow;
      // return null;
    } finally {
      streamReader.cancel();
    }
  }

  Stream<List<int>> _getChunkStream() => streamReader.readStream(_maxChunkSize);

  // Updating total upload progress
  void _progress(int current, int total) {
    double currentProgress =
        double.tryParse((current / total).toStringAsFixed(2)) ?? 0.0;
    if (currentProgress >= 1) {
      currentProgress = 1;
      task
        ..status = TaskStatus.complete.name
        ..progress = 1.0;
    } else {
      task
        ..progress = currentProgress
        ..status = TaskStatus.running.name;
    }
    debugPrint('Progress: $currentProgress total:$total');
    onUploadProgress?.call(task, currentProgress);
  }

  void _updateProgress(int chunkIndex, int chunkCurrent, int chunkTotal) {
    int currentSize = (chunkIndex * _maxChunkSize) + chunkCurrent;
    double currentProgress =
        double.tryParse((currentSize / task.fileSize).toStringAsFixed(2)) ??
            0.0;
    if (currentProgress > 1) {
      currentProgress = 1;
      task
        ..status = TaskStatus.complete.name
        ..progress = 1.0;
    } else {
      task
        ..progress = currentProgress
        ..status = TaskStatus.running.name;
    }
    debugPrint(
        'Progress: $currentProgress currentSize:$currentSize total:${task.fileSize}');
    onUploadProgress?.call(task, currentProgress);
  }

  // Returning start byte offset of current chunk
  int _getChunkStart(int chunkIndex) => chunkIndex * _maxChunkSize;

  // Returning end byte offset of current chunk
  int _getChunkEnd(int chunkIndex) =>
      min((chunkIndex + 1) * _maxChunkSize, task.fileSize);

  // Returning chunks count based on file size and maximum chunk size
  int get _chunksCount => (task.fileSize / _maxChunkSize).ceil();
}

// Based on RFC 7233 (https://tools.ietf.org/html/rfc7233#section-2)
final ChunkHeadersCallback _defaultHeadersCallback =
    (int start, int end, int fileSize) =>
        {'Content-Range': 'bytes $start-${end - 1}/$fileSize'};
